package nl.science.uva.pico.compiler.stacklanguage;

import java.util.ArrayList;
import java.util.List;
import java.util.Collections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import nl.uva.science.pico.picolanguage.types.EXP;
import nl.uva.science.pico.picolanguage.types.PICOID;
import nl.uva.science.pico.picolanguage.types.exp.Add;
import nl.uva.science.pico.picolanguage.types.exp.Bracket;
import nl.uva.science.pico.picolanguage.types.exp.Concatenate;
import nl.uva.science.pico.picolanguage.types.exp.NumberValue;
import nl.uva.science.pico.picolanguage.types.exp.StringValue;
import nl.uva.science.pico.picolanguage.types.exp.Subtract;
import nl.uva.science.pico.picolanguage.types.idtype.Variable;
import nl.uva.science.pico.stacklanguage.Factory;
import nl.uva.science.pico.stacklanguage.types.Instr;
import nl.uva.science.pico.stacklanguage.types.InstrList;
import nl.uva.science.pico.stacklanguage.types.LabelInstr;
import nl.uva.science.pico.stacklanguage.types.LabelInstrList;

/**
 * Factory to create stack machine instructions. 
 * 
 * @author Ivo van Dongen, Karel Pieterson
 *
 * @version 1.0
 */
public class StackInstructionFactory {
	private VariableIndexMap variableIndexMap;
	private Factory stacklanguageFactory;
	private LabelCounter labelCounter;
	
	private Logger logger = LoggerFactory.getLogger(StackInstructionFactory.class);

	/**
	 * 
	 * @param stacklanguageFactory the stack language api factory
	 */
	public StackInstructionFactory(Factory stacklanguageFactory) {
		this.stacklanguageFactory = stacklanguageFactory;
		this.variableIndexMap = new VariableIndexMap();
		this.labelCounter = new LabelCounter();
	}
	
	/**
	 * Creates an {@link InstrList} from the given {@link List} of {@link Instr}
	 * 
	 * @param labelInstructions
	 * @return the list containing the generated instructions
	 */
	public InstrList createInstrList(List<LabelInstr> labelInstructions) {
		if(labelInstructions == null) {
			throw new NullPointerException("List is null");
		}

		logger.debug("Creating InstrList");

		//Reverse the list
		Collections.reverse(labelInstructions);
		//Wrap the output in a InstrList (reverses it again)
		LabelInstrList labelInstrList = stacklanguageFactory.makeLabelInstrList();

		for (LabelInstr instruction : labelInstructions){
			labelInstrList = stacklanguageFactory.makeLabelInstrList(instruction, labelInstrList);
		}
		
		//Return the generated instructions
		return stacklanguageFactory.makeInstrList_InstrList(labelInstrList);
	}
	
	/**
	 * Creates the instructions to deal with an expression
	 * 
	 * @param expression the {@link EXP}
	 * @return the {@link List} of {@link LabelInstr} representing the expression
	 */
	public List<LabelInstr> createExpInstructionList(EXP expression) {
		//Check input
		if(expression == null) {
			throw new NullPointerException("Expression is null!");
		}
		
		logger.debug("Processing expression: " + expression);

		//Create a container for generated instructions
		List<LabelInstr> instructionList = new ArrayList<LabelInstr>();
		//Check the type and process accordingly
		//Operators
		if (expression instanceof Add) {
			instructionList.addAll(createAddInstruction((Add) expression));
		} else if (expression instanceof Subtract) {
			instructionList.addAll(createSubtractInstruction((Subtract) expression));
		} else if (expression instanceof nl.uva.science.pico.picolanguage.types.exp.Variable) {
			instructionList.addAll(createLoadInstruction((nl.uva.science.pico.picolanguage.types.exp.Variable) expression));
		} else if (expression instanceof Concatenate) {
			instructionList.addAll(createConcatenateInstruction((Concatenate) expression));
		//Variables
		} else if (expression instanceof NumberValue) {
			instructionList.addAll(createNumberConstantInstruction((NumberValue) expression));
		} else if (expression instanceof StringValue) {
			instructionList.addAll(createStringConstantInstruction((StringValue) expression));
		//Other
		} else if (expression instanceof Bracket) {
			throw new UnsupportedOperationException("TODO: implement: " + Bracket.class);
		//Problem
		} else {
			throw new UnsupportedOperationException(expression.getClass() + " is not supported by this compiler.");
		}
		
		return instructionList;
	}
	
	/**
	 * Creates stack instructions to initialize a variable
	 * 
	 * @param variable the {@link Variable} to declare
	 * @return the {@link LabelInstr} representation of the variable declaration
	 */
	public LabelInstr createDeclaration(Variable variable) {
		Instr instr = null;
		
		logger.debug("Creating declaration for variable: " + variable);

		//Create either a natural or string variable with default value
		if(variable.getVariableType().isNatural()) {
			instr = stacklanguageFactory.makeInstr_Nat(0);
		} else if(variable.getVariableType().isString()) {
			instr = stacklanguageFactory.makeInstr_Str("");
		//Problem, unknown variable type
		} else {
			throw new IllegalArgumentException("Unsupported type: " + variable.getName());
		}
		
		//Add to variable index map
		variableIndexMap.addVariable(variable.getVariableName().getString());
		//Wrap and return the generated instruction
		return stacklanguageFactory.makeLabelInstr_LabelInstr(labelCounter.getNext(), instr);
	}
	
	/**
	 * Creates stack  instructions to store to a specific variable position on the stack
	 * 
	 * @param variableId
	 * @return the stack language program
	 */
	public List<LabelInstr> createStoreInstruction(PICOID variableId) {
		//Check input
		if (variableId == null) {
			throw new NullPointerException("Null variable reference!");
		}

		logger.debug("Creating store instructions for variable: " + variableId);

		List<LabelInstr> instructionList = new ArrayList<LabelInstr>();
		//Add the stack index for the variable
		int variableStackIndex = variableIndexMap.getIndex(variableId.getString());
		instructionList.add(makeLabelInstr(stacklanguageFactory.makeInstr_Nat(variableStackIndex)));
		//Add the store instruction
		instructionList.add(makeLabelInstr(stacklanguageFactory.makeInstr_Store()));
		return instructionList;
	}
	
	/**
	 * Creates stack instructions to deal with a string value
	 * 
	 * @param value
	 * @return String instruction
	 */
	public List<LabelInstr> createStringConstantInstruction(StringValue value) {
		if(value == null) {
			throw new NullPointerException("Null value");
		}
		
		logger.debug("Creating instructions for string: " + value);
		
		return newLabelInstrList(makeLabelInstr(stacklanguageFactory.makeInstr_Str(value.getStringValue())));
	}

	/**
	 * Creates stack instructions to deal with a natural value
	 * 
	 * @param value
	 * @return number instruction
	 */
	public List<LabelInstr> createNumberConstantInstruction(NumberValue value) {
		if(value == null) {
			throw new NullPointerException("Null value");
		}
		
		logger.debug("Creating instructions for number value: " + value);
		
		return newLabelInstrList(makeLabelInstr(stacklanguageFactory.makeInstr_Nat(value.getNumberValue())));
	}
	
	/**
	 * Creates a pointer to the current label + the offset
	 * 
	 * @param offset the offset (a negative offset is possible)
	 * @return offset label reference instruction
	 */
	public LabelInstr createLabelReferenceInstruction(int offset) {
		//The resulting label
		int referencedLabel = labelCounter.getCurrent() + offset;
		
		//Check for a legal value
		if(referencedLabel < 1) {
			throw new IllegalArgumentException("Creates a negative index: " + referencedLabel);
		}
		
		logger.debug("Creating label reference instruction for offset: " + offset + " resulting in label: " + referencedLabel);
		
		//Wrap and return the instruction
		return makeLabelInstr(stacklanguageFactory.makeInstr_Nat((labelCounter.getCurrent() + offset)));
	}

	/**
	 * @param concatenate
	 * @return concatenate instructions 
	 */
	public List<LabelInstr> createConcatenateInstruction(Concatenate concatenate) {
		//Check input
		if(concatenate == null) {
			throw new NullPointerException("Concatenate is null!");
		}
		if(concatenate.getChildCount() != 2) {
			throw new IllegalStateException("Concatenate can only have two members, not: " + concatenate.getChildCount() + " in: " + concatenate);
		}
		
		logger.debug("Creating concat instructions for: " + concatenate);
		
		//For expression: EXP1, EXP2, Concat
		//EXP1
		List<LabelInstr> instructions = createExpInstructionList((EXP) concatenate.getChildAt(0));
		//EXP2
		instructions.addAll(createExpInstructionList((EXP) concatenate.getChildAt(1)));
		//Concat
		instructions.add(makeLabelInstr(stacklanguageFactory.makeInstr_Concat()));
		
		return instructions;
	}

	/**
	 * Creates instructions to load the value of the variable to the top of the stack
	 * 
	 * @param variable
	 * @return load instructions
	 */
	public List<LabelInstr> createLoadInstruction(nl.uva.science.pico.picolanguage.types.exp.Variable variable) {
		//Check input
		if (variable == null) {
			throw new NullPointerException("Variable is null!");
		}
		if(variable.getChildCount() == 0) {
			throw new IllegalArgumentException("No member found in variable: " + variable);
		}
		if (!(variable.getChildCount() == 1 && (variable.getChildAt(0) instanceof PICOID))) {
			throw new IllegalArgumentException("Load instruction can only have one member: the variable name, not: " +
												variable.getChildAt(0) + " as in: " + variable);
		}
		
		logger.debug("Loading var: " + variable + " - " + this);
		
		List<LabelInstr> instructionList = new ArrayList<LabelInstr>();

		//Add the stack index for the variable
		int variableStackIndex = variableIndexMap.getIndex(((PICOID)variable.getChildAt(0)).getString());
		instructionList.add(makeLabelInstr(stacklanguageFactory.makeInstr_Nat(variableStackIndex)));
		
		//Add the load instruction
		instructionList.add(makeLabelInstr(stacklanguageFactory.makeInstr_Load()));
		
		return instructionList;
	}

	/**
	 * Creates the sequence of stack instructions to do a subtraction of two expressions
	 * 
	 * @param subtract
	 * @return generated subtraction instructions
	 */
	public List<LabelInstr> createSubtractInstruction(Subtract subtract) {
		//Check input
		if(subtract == null) {
			throw new NullPointerException("Subtract is null!");
		}
		if(subtract.getChildCount() != 2) {
			throw new IllegalStateException("Subtract can only have two members, not: " + subtract.getChildCount() + " in: " + subtract);
		}
		
		logger.debug("Creating substract instructions for: " + subtract);
		
		//For expression: EXP1, EXP2, Negate, Add
		//EXP1
		List<LabelInstr> instructions = createExpInstructionList((EXP) subtract.getChildAt(0));
		//EXP2
		instructions.addAll(createExpInstructionList((EXP) subtract.getChildAt(1)));
		//Negate
		instructions.add(makeLabelInstr(stacklanguageFactory.makeInstr_Minus()));
		//Add
		instructions.add(makeLabelInstr(stacklanguageFactory.makeInstr_Add()));
		
		return instructions;
	}

	/**
	 * Creates the instructions to do an addition on the stack
	 * 
	 * @param add
	 * @return generated addition instructions
	 */
	public List<LabelInstr> createAddInstruction(Add add) {
		//Check input
		if(add == null) {
			throw new NullPointerException("Add is null!");
		}
		if(add.getChildCount() != 2) {
			throw new IllegalStateException("Add can only have two members, not: " + add.getChildCount() + " in: " + add);
		}
		
		logger.debug("Creating add instructions for: " + add);
		
		//For expression: EXP1, EXP2, Add
		//EXP1
		List<LabelInstr> instructions = createExpInstructionList((EXP) add.getChildAt(0));
		//EXP2
		instructions.addAll(createExpInstructionList((EXP) add.getChildAt(1)));
		//Add
		instructions.add(makeLabelInstr(stacklanguageFactory.makeInstr_Add()));
		
		return instructions;
	}
	
	/**
	 * Skip instruction is a NOP; needed as place holder
	 * 
	 * @return a Skip instruction
	 */
	public LabelInstr createSkipInstruction() {
		
		logger.debug("Creating skip instruction");
		
		return makeLabelInstr(stacklanguageFactory.makeInstr_Skip());
	}
	
	/**
	 * Creates comparison instructions to the current stack. 
	 * The stack must contain two values: the value to compare to and the address to jump to)
	 * 
	 * @param compareValue
	 * @return generated comparison instructions
	 */
	public List<LabelInstr> createIfEqualToCurrentStack(int compareValue) {
		
		logger.debug("Creating ifequal for value: " + compareValue);
		
		List<LabelInstr> instructions = newLabelInstrList(makeLabelInstr(stacklanguageFactory.makeInstr_Nat(compareValue)));
		instructions.add(makeLabelInstr(stacklanguageFactory.makeInstr_Ifequal()));
		
		return instructions;
	}
	
	/**
	 * Creates a goto statement to the supplied reference label
	 * 
	 * @param referenceLabel the label to goto
	 * @return generated goto instructions
	 */
	public List<LabelInstr> createGotoInstruction(LabelInstr referenceLabel) {
		if(referenceLabel == null) {
			throw new NullPointerException("Reference label is null!");
		}
		
		logger.debug("Creating goto reference label: " + referenceLabel);
		
		List<LabelInstr> instructions = newLabelInstrList(makeLabelInstr(stacklanguageFactory.makeInstr_Nat(referenceLabel.getNatCon())));
		instructions.add(makeLabelInstr(stacklanguageFactory.makeInstr_Goto()));

		return instructions;
	}
	
	/**
	 * Creates a stop instruction
	 * 
	 * @return generated stop instruction
	 */
	public LabelInstr createStopInstruction() {

		logger.debug("Creating stop instruction");

		return makeLabelInstr(stacklanguageFactory.makeInstr_Stop());
	}
	
	/**
	 * Wraps the {@link Instr} in a {@link LabelInstr} with an unique label value
	 * 
	 * @param instruction the {@link Instr} to wrap
	 * @return the instruction, prefixed with a unique label (label is an integer value used as reference)
	 */
	private LabelInstr makeLabelInstr(Instr instruction) {
		return stacklanguageFactory.makeLabelInstr_LabelInstr(labelCounter.getNext(), instruction);
	}
	
	/**
	 * Convenience method to create a {@link List} with one {@link LabelInstr}
	 * 
	 * @param labelInstr the {@link LabelInstr}
	 * @return the {@link List}
	 */
	private List<LabelInstr> newLabelInstrList(LabelInstr labelInstr) {
		List<LabelInstr> list = new ArrayList<LabelInstr>();
		list.add(labelInstr);
		return list;
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return 	"Variable index: " + variableIndexMap;
	}

	/**
	 * Getter for the variableIndexMap object
	 * 
	 * @return the map containing the variable indices
	 */
	public VariableIndexMap getVariableIndexMap() {
		return variableIndexMap;
	}
}
